using Gma.QrCodeNet.Encoding.DataEncodation;

namespace Gma.QrCodeNet.Encoding.Versions;

internal static class VersionControl
{
	private const int NumBitsModeIndicator = 4;
	private const string DefaultEncoding = QRCodeConstantVariable.DefaultEncoding;

	private static readonly int[] VERSION_GROUP = new int[] { 9, 26, 40 };

	/// <summary>
	/// Determine which version to use
	/// </summary>
	/// <param name="dataBitsLength">Number of bits for encoded content</param>
	/// <param name="encodingName">Encoding name for EightBitByte</param>
	/// <returns>VersionDetail and ECI</returns>
	internal static VersionControlStruct InitialSetup(int dataBitsLength, ErrorCorrectionLevel level, string encodingName)
	{
		int totalDataBits = dataBitsLength;

		bool containECI = false;

		BitList eciHeader = new();

		if (encodingName is not DefaultEncoding and not QRCodeConstantVariable.UTF8Encoding)
		{
			ECISet eciSet = new(ECISet.AppendOption.NameToValue);
			int eciValue = eciSet.GetECIValueByName(encodingName);

			totalDataBits += ECISet.NumOfECIHeaderBits(eciValue);
			eciHeader = eciSet.GetECIHeader(encodingName);
			containECI = true;
		}

		// Determine which version group it belong to
		int searchGroup = DynamicSearchIndicator(totalDataBits, level);

		int[] charCountIndicator = CharCountIndicatorTable.GetCharCountIndicatorSet();

		totalDataBits += (NumBitsModeIndicator + charCountIndicator[searchGroup]);

		int lowerSearchBoundary = searchGroup == 0 ? 1 : (VERSION_GROUP[searchGroup - 1] + 1);
		int higherSearchBoundary = VERSION_GROUP[searchGroup];

		// Binary search to find proper version
		int versionNum = BinarySearch(totalDataBits, level, lowerSearchBoundary, higherSearchBoundary);

		VersionControlStruct vcStruct = FillVCStruct(versionNum, level);

		vcStruct.IsContainECI = containECI;

		vcStruct.ECIHeader = eciHeader;

		return vcStruct;
	}

	private static VersionControlStruct FillVCStruct(int versionNum, ErrorCorrectionLevel level)
	{
		if (versionNum is < 1 or > 40)
		{
			throw new InvalidOperationException($"Unexpected version number: {versionNum}");
		}

		VersionControlStruct vcStruct = new();

		int version = versionNum;

		QRCodeVersion versionData = VersionTable.GetVersionByNum(versionNum);

		int numTotalBytes = versionData.TotalCodewords;

		ErrorCorrectionBlocks ecBlocks = versionData.GetECBlocksByLevel(level);
		int numDataBytes = numTotalBytes - ecBlocks.NumErrorCorrectionCodewards;
		int numECBlocks = ecBlocks.NumBlocks;

		VersionDetail vcDetail = new(version, numTotalBytes, numDataBytes, numECBlocks);

		vcStruct.VersionDetail = vcDetail;
		return vcStruct;
	}

	/// <summary>
	/// Decide which version group it belong to
	/// </summary>
	/// <param name="numBits">Number of bits for bitlist where it contain DataBits encode from input content and ECI header</param>
	/// <param name="level">Error correction level</param>
	/// <returns>Version group index for VERSION_GROUP</returns>
	private static int DynamicSearchIndicator(int numBits, ErrorCorrectionLevel level)
	{
		int[] charCountIndicator = CharCountIndicatorTable.GetCharCountIndicatorSet();
		int loopLength = VERSION_GROUP.Length;
		for (int i = 0; i < loopLength; i++)
		{
			int totalBits = numBits + NumBitsModeIndicator + charCountIndicator[i];

			QRCodeVersion version = VersionTable.GetVersionByNum(VERSION_GROUP[i]);
			int numECCodewords = version.GetECBlocksByLevel(level).NumErrorCorrectionCodewards;

			int dataCodewords = version.TotalCodewords - numECCodewords;

			if (totalBits <= dataCodewords * 8)
			{
				return i;
			}
		}

		throw new InputOutOfBoundaryException($"QRCode do not have enough space for {(numBits + NumBitsModeIndicator + charCountIndicator[2])} bits");
	}

	/// <summary>
	/// Use number of data bits(header + eci header + data bits from EncoderBase) to search for proper version to use
	/// between min and max boundary.
	/// Boundary define by DynamicSearchIndicator method.
	/// </summary>
	private static int BinarySearch(int numDataBits, ErrorCorrectionLevel level, int lowerVersionNum, int higherVersionNum)
	{
		int middleVersionNumber;

		while (lowerVersionNum <= higherVersionNum)
		{
			middleVersionNumber = (lowerVersionNum + higherVersionNum) / 2;
			QRCodeVersion version = VersionTable.GetVersionByNum(middleVersionNumber);
			int numECCodewords = version.GetECBlocksByLevel(level).NumErrorCorrectionCodewards;
			int dataCodewords = version.TotalCodewords - numECCodewords;

			if (dataCodewords << 3 == numDataBits)
			{
				return middleVersionNumber;
			}

			if (dataCodewords << 3 > numDataBits)
			{
				higherVersionNum = middleVersionNumber - 1;
			}
			else
			{
				lowerVersionNum = middleVersionNumber + 1;
			}
		}
		return lowerVersionNum;
	}
}
